#include "iConfig.hpp"

#include <glf/io/fileStreamImpl.h>
#include <glf/io/fs.h>
#include <glf/io/crcChecker.h>
#include <glf/core/timer.h>

#include <glf/core/condition.h>
#include <glf/core/mutex.h>
#include <glf/core/fastMutex.h>

#if GLF_ENABLE_DEBUGGER
#	include <glf/debugger/profiler.h>
#endif

#include <deque>

#undef Yield

namespace glf {

struct FileStreamImplCallback {
#if GLF_VIRTUAL_FILESTREAM
	FileStream* fs;
	FileStreamImpl* fs_compare;
#else
	FileStreamImpl* fs;
	FileStreamImpl* fs_compare;
#endif

	FileStreamImpl::Callback callback;
	void* buffer;
	size_t offset;
	size_t size;
	void* arg;
};

FastMutex gDequeMutex;
Mutex gThreadMutex;
Condition gThreadCondition(gThreadMutex);
std::deque<FileStreamImplCallback*> gQueuedReads;

bool WaitAllDone(const FileStreamImpl* fs_compare, int ms) {
	int64 time = GetMilliseconds();
	bool infiniteWait = ms < 0;
	
	do {
		bool found = false;
		{
			glf::FastLockScope lock(gDequeMutex);
		
			for(size_t i = 0; i < gQueuedReads.size(); ++i) {
				FileStreamImplCallback* sfc = gQueuedReads[i];
				
				if(sfc->fs_compare == fs_compare) {
					found = true;
					break;
				}
			}
		}

		if(found) {
			Thread::Sleep(5);
		} else {
			return true;
		}
	} while(infiniteWait || GetMilliseconds() - time < ms);
	
	return false;
}

struct ReadASyncThread : public glf::Thread4k {
	virtual void Run() {
		while(true) {
			FileStreamImplCallback* sfc = 0;
			{
				FastLockScope lock(gDequeMutex);
				if(!gQueuedReads.empty()) {
					sfc = gQueuedReads.front();
				}
			}
			if(sfc) {
				char* buffer = static_cast<char*>(sfc->buffer);
				size_t readSoFar = 0;

				int result = 0;

				{
					ProfilerScope("read");
					while(readSoFar < sfc->size) {
						size_t read	= Min(sfc->size - readSoFar, size_t(16 * 1024)); 
						int offset	= sfc->offset + readSoFar;
						
						int readResult = sfc->fs->ReadWithOffset(buffer, read, offset);
						
						if(readResult > 0) {
							result += readResult;
							
						} else {
							result = -1;
							break;
						}

						buffer += readResult;
						readSoFar += readResult;
						
						if(readSoFar < sfc->size) {
							Thread::Yield();
							//Thread::Sleep(1);
						}
					}
				}
				
				if(sfc->callback) {
					ProfilerScope("callback");
					(*sfc->callback)(result, *sfc->fs, sfc->arg);
				}
				
#if GLF_VIRTUAL_FILESTREAM
				--sfc->fs->mFileStream.mAsyncCount;
#else
				--sfc->fs->mAsyncCount;
#endif
				
				{
					FastLockScope lock(gDequeMutex);
					gQueuedReads.pop_front();
				}
				delete sfc;
			} else {
				
				gThreadMutex.Lock();
				gThreadCondition.Wait(1000);
				gThreadMutex.Unlock();
			}
		}
	}
} gReadASyncThread;

void FileStreamImpl::DispatchDoneReads() {
}

struct FileStreamImplBase : public ios { 
	FileStreamImpl& mFileStream;
	        
	FileStreamImplBase(FileStreamImpl& fileStream) :
		mFileStream(fileStream)
	{       
	}  
	virtual ~FileStreamImplBase() {
	}

	virtual bool Open(const char* filename, openflag flag) {
		return false;
	}
	virtual void Close() {
	}
	virtual bool Seek(int offset, seek origin) {
		return false;
	}
	virtual size_t Tell() const {
		return 0;
	}
	virtual size_t Read(void* buffer, size_t size) {
		return 0;
	}
	virtual size_t ReadWithOffset(void* buffer, size_t size, size_t offset) {
		return 0;
	}
	virtual size_t Write(const void* buffer, size_t size) {
		return 0;
	}
};

}


//small hack because I dont know how to modify ALL PROJECT FILE TO ADD THOSE CPP...
//I had to comment out the #include "iconfig.hpp" in the .cpp file so HACK_HARDCODED_COMPILE_UNIT_ADDED_CPP 
//#wrapped could be remove if those cpp are included in project file...
#define HACK_HARDCODED_COMPILE_UNIT_ADDED_CPP 1
#include "archiveManager.cpp"
#include "core/zip_reader.cpp"
#undef HACK_HARDCODED_COMPILE_UNIT_ADDED_CPP

//important, this must be before GLFi_HEADER_API, the system .hpp need those #define.
#if GLF_USE_FILESTREAM_ARCHIVE 
	#define SYSFILESTREAM_COMPLETE_CLASSNAME FileStreamSystemImpl
	#define SYSFILESTREAM_CLASSNAME SYSFILESTREAM_COMPLETE_CLASSNAME
#else
	#define SYSFILESTREAM_COMPLETE_CLASSNAME FileStreamImpl::Impl
	#define SYSFILESTREAM_CLASSNAME Impl
#endif

#if __APPLE__
	#include "fileStreamImpl_posix_.hpp"
#else
	#include GLFi_HEADER_API(fileStreamImpl, GLF_API_IO)
#endif

#if GLF_USE_FILESTREAM_ARCHIVE 
#if __APPLE__
	#include "fileStreamImpl_arc_.hpp"
#else
	#include GLFi_HEADER_API(fileStreamImpl, arc)
#endif

#endif


namespace glf {

FileStreamImpl::FileStreamImpl() :
	IOStream(),
	mAsyncCount(0),
	m(GLF_NEW_IMPL(*this)) {
	GLF_CHECK_IMPL_SIZE();
}

FileStreamImpl::FileStreamImpl(const char* filename, openflag flag) :
	IOStream(),
	mAsyncCount(0),
	m(GLF_NEW_IMPL(*this)) {
	Open(filename, flag);
}

FileStreamImpl::~FileStreamImpl() {
	Close();
	GLF_DELETE_IMPL();
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool FileStreamImpl::_Open(const char* filename, openflag flag, bool isUsingArchive) {
	if(mIsBusy) {
		mLastError = fs::ERROR_ISBUSY;
		return false;
	}
	Close();
	mFlag = flag;

#if GLF_ENABLE_DEBUGGER
	debugger::StreamScopeEvent dbgEvent(debugger::sStreamEventType, "[glf] FileStreamImpl::Open", filename);
#endif
	

	std::vector<char> fullPathBuffer(2048);
	flag = fs::ResolvePath(filename, flag, &fullPathBuffer[0], fullPathBuffer.size());
	// remove multiple "./" at the beginning, they will mess up opening archives
	const char* pathPtr = &fullPathBuffer[0];
	while(strlen(pathPtr) > 1 && pathPtr[0] == '.' && (pathPtr[1] == '/' || pathPtr[1] == '\\'))
		pathPtr +=2;
	mFullPath = pathPtr;

    if((flag & ios::obfuscated) != 0){
        mFlag |= ios::obfuscated;
    }
    if((flag & ios::check_crc) != 0){
        mFlag |= ios::check_crc;
    }
	stringc buffer;

	if(TestFlag(flag, loc_data)) {

#if GLF_HOME_DATA_OVERRIDE
		if(!TestFlag(mFlag, write) && !TestFlag(mFlag, atend) && !TestFlag(mFlag, trunc) && !TestFlag(mFlag, create)) {
			stringc overridePath = JoinPath(fs::GetHomeDir(), "data");
			// create data override folder, if does not exist
			if(!fs::DoesFileExist(overridePath.c_str())) {
				fs::MkDir(overridePath.c_str(), ios::loc_home);
			}

			buffer = JoinPath(overridePath , mFullPath);
			mIsOpened = m.Open(buffer.c_str(), flag);
			#if GLF_USE_FILESTREAM_CALLBACK
				if(GetFileStreamCallback()) {GetFileStreamCallback()->OnOpen(this, buffer.c_str(), flag, mIsOpened);}
			#endif
			if(mIsOpened) {
				if(CheckCrc(filename))
				{
					return mIsOpened;
				}
				else
				{
					return false;
				}
			}
		}
#endif

		for(size_t i = 0; i < fs::GetAlternateDataDirCount(); ++i) {
			const char* altDir = fs::GetAlternateDataDir(i);
			if(altDir == 0 || strlen(altDir) == 0) { 
				continue;
			}
			buffer = JoinPath(altDir , mFullPath);
			mIsOpened = m.Open(buffer.c_str(), flag);
			#if GLF_USE_FILESTREAM_CALLBACK
				if(GetFileStreamCallback()) {GetFileStreamCallback()->OnOpen(this, buffer.c_str(), flag, mIsOpened);}
			#endif
			if(mIsOpened) {
                if(CheckCrc(filename))
                {
    				return mIsOpened;
                }
                else
                {
                    return false;
                }
			}
		}
	}
	
	mIsOpened = m.Open(mFullPath.c_str(), flag);
#if GLF_USE_FILESTREAM_CALLBACK
		if(GetFileStreamCallback()) {GetFileStreamCallback()->OnOpen(this, mFullPath.c_str(), flag, mIsOpened);}
#endif
    if(mIsOpened && !CheckCrc(filename))
    {
        return false;
    }
		
	mLastError = glf::fs::ERROR_NONE;

	return mIsOpened;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool FileStreamImpl::Open(const char* filename, openflag flag) {
	
	bool opened = false;
	// search in archive = strip path
	if(glf::ArchiveManager::GetInstance()->IsUsingArchive(flag))
	{
		std::string spath(filename);
		size_t pos = spath.find_last_of("/\\");
		if(pos != std::string::npos)
		{
			opened = _Open(spath.substr(pos+1).c_str(), flag, true);
		}
	}

	// if not found, search normally
	if(!opened)
	{
		opened = _Open(filename, flag, false);
	}

	return opened;
}

void FileStreamImpl::Close() {

#if GLF_VIRTUAL_FILESTREAM == 0
	WaitASync(size_t(-1));
#endif

#if GLF_ENABLE_DEBUGGER
	debugger::StreamScopeEvent dbgEvent(debugger::sStreamEventType, "[glf] FileStreamImpl::Close");
#endif

	m.Close();
#if GLF_USE_FILESTREAM_CALLBACK
	if(GetFileStreamCallback() && mIsOpened) {GetFileStreamCallback()->OnClose(this, mFullPath.c_str());}
#endif

    mIsOpened = false;
}

bool FileStreamImpl::CheckCrc(const char* filename){
	if((mFlag & ios::check_crc) != 0 && glf::CrcChecker::HasFileEntry(filename))
	{
		int size = GetSize();
		std::vector<char> buf(size);
		char* buffer = &buf[0];
		size_t pos = Tell();
		Seek(0, glf::ios::beg);
		Read(buffer, size);
		Seek(pos, glf::ios::beg);
		bool result = glf::CrcChecker::DoCrcCheck(filename, buffer, size);

		if(!result)
		{
            Console::Println("Corrupted file : %s", filename);
            mLastError = glf::fs::ERROR_CORRUPT;
			Close();

            return false;
		}
	}
    return true;
}

size_t FileStreamImpl::GetSize() const {
	return mFilePosition.GetFileSize();
}

bool FileStreamImpl::Seek(int offset, seek origin) {
	if(mIsBusy) {
		mLastError = glf::fs::ERROR_ISBUSY;
		return false;
	}

	mFilePosition.Seek(offset, origin);
	
	bool result = m.Seek(offset, origin);

#if GLF_USE_FILESTREAM_CALLBACK
	if(GetFileStreamCallback()) {GetFileStreamCallback()->OnSeek(this, mFullPath.c_str(), offset, origin, result);}
#endif
#if GLF_USE_FILESTREAM_DBGSTATS
	mDebugStats.numSeeks++;
#endif
	mLastError = glf::fs::ERROR_NONE;

	return result;
}

size_t FileStreamImpl::Peek(void* buffer, size_t size) {
	if(mIsBusy) {
		mLastError = glf::fs::ERROR_ISBUSY;
		return size_t(-1);
	}
	size_t offset = Tell();
	int read = Read(buffer, size);
	Seek(offset, beg);

	mLastError = glf::fs::ERROR_NONE;

	return read;
}

bool FileStreamImpl::CanSeek() const {
	return true;
}

bool FileStreamImpl::CanRead() const {
	return (mFlag & read) != 0;
}

bool FileStreamImpl::CanWrite () const {
	return (mFlag & write) != 0;
}

size_t FileStreamImpl::Tell()  const {
	return m.Tell();
}

bool FileStreamImpl::IsEof() const {
	return Tell() == mFilePosition.GetFileSize();
}

bool FileStreamImpl::CancelAsync(Callback callback, void* arg) {
	glf::FastLockScope lock(gDequeMutex);

	for(std::deque<FileStreamImplCallback*>::iterator it = gQueuedReads.begin(); it != gQueuedReads.end(); ++it) {

		FileStreamImplCallback* fsc = *it;
		
		if(fsc->callback == callback && fsc->arg == arg) {
			gQueuedReads.erase(it);
			return true; //what if there are 2 callbacks with same signature?!?
		}
	}
	
	return false;
}

int FileStreamImpl::WaitASync(size_t ms) const {
	
	if (mAsyncCount != 0)
	{
		return WaitAllDone(this, ms);
	}

	return 1;
}


#if GLF_VIRTUAL_FILESTREAM
bool FileStreamImpl::ReadASync(FileStream* fs, void* buffer, size_t size, size_t offset, FileStreamImpl::Callback callback, void* arg) {
#else
bool FileStreamImpl::ReadASync(void* buffer, size_t size, size_t offset, FileStreamImpl::Callback callback, void* arg) {
#endif
	++mAsyncCount;

#if GLF_ENABLE_DEBUGGER
	debugger::StreamScopeEvent dbgEvent(debugger::sStreamEventType, "[glf] FileStreamImpl::ReadASync");
#endif

	FileStreamImplCallback* fsc = new FileStreamImplCallback;
	memset(fsc, 0, sizeof(FileStreamImplCallback));

#if GLF_VIRTUAL_FILESTREAM
	fsc->fs = fs;
	fsc->fs_compare = this;
#else
	fsc->fs = this;
	fsc->fs_compare = this;
#endif
	fsc->buffer = buffer;
	fsc->size = size;
	fsc->offset = offset;
	fsc->callback = callback;
	fsc->arg = arg;

	{
		glf::FastLockScope lock(gDequeMutex);
	
		gQueuedReads.push_back(fsc);
	}

	if(gReadASyncThread.GetStatus() == Thread::STATUS_STOPPED) {
		gReadASyncThread.Start(Thread::GetPriorityNorm() + 1);
	} else {
		gThreadCondition.Signal();
	}

	return true;	
}
/*
bool FileStreamImpl::ReadASync(void* buffer, size_t size, Callback callback, void* arg) {
	if(mIsBusy) {
		mLastError = glf::fs::ERROR_ISBUSY;
		return false;
	}

	mCallbackResult = 0;
	mCallback = callback;
	mCallbackArg = arg;
	mIsBusy = true;
	
	if(m.ReadASync(buffer, size)) {
		mLastError = glf::fs::ERROR_NONE;
		return true;
	}
	
	mIsBusy = false;

	return false;
}
*/
size_t FileStreamImpl::ReadWithOffset(void* buffer, size_t size, size_t offset) {
	return m.ReadWithOffset(buffer, size, offset);	
}

size_t FileStreamImpl::Read(void* buffer, size_t length)  {
	if(mIsBusy) {
		mLastError = glf::fs::ERROR_ISBUSY;
		return size_t(-1);
	}

#if GLF_ENABLE_DEBUGGER
	debugger::StreamScopeEvent dbgEvent(debugger::sStreamEventType, "[glf] FileStreamImpl::Read");
#endif

    size_t startPos = (mFlag & ios::obfuscated) != 0 ? Tell() : 0;
	int read = m.Read(buffer, length);
	
#if GLF_USE_FILESTREAM_CALLBACK
	if(GetFileStreamCallback()) {GetFileStreamCallback()->OnRead(this, mFullPath.c_str(), buffer, length, read);}
#endif
#if GLF_USE_FILESTREAM_DBGSTATS
	mDebugStats.numReads++;
	mDebugStats.numBytesRead+=read;
#endif

	if(read >= 0) {
		mFilePosition.Skip(read);
	}

	mLastError = glf::fs::ERROR_NONE;

	if( (mFlag & ios::obfuscated) != 0 && startPos < 4 )
	{
		for(int i = 0, end = Min<size_t>(4 - startPos, length); i < end; ++i)
		{
			reinterpret_cast<unsigned char*>(buffer)[i] -= int(startPos) + i + 1;
		}
	}

	return read;
}

size_t FileStreamImpl::Write(const void* buffer, size_t size) {
	if(mIsBusy) {
		mLastError = fs::ERROR_ISBUSY;
		return size_t(-1);
	}

#if GLF_ENABLE_DEBUGGER
	debugger::StreamScopeEvent dbgEvent(debugger::sStreamEventType, "[glf] FileStreamImpl::Write");
#endif

	int written = m.Write(buffer, size);
#if GLF_USE_FILESTREAM_CALLBACK
	if(GetFileStreamCallback()) {GetFileStreamCallback()->OnWrite(this, mFullPath.c_str(), buffer, size, written);}
#endif 

#if GLF_USE_FILESTREAM_DBGSTATS
	mDebugStats.numWrites++;
	mDebugStats.numBytesWritten+=written;
#endif
	if(written >= 0) {
		mFilePosition.Append(written);
	}

	mLastError = glf::fs::ERROR_NONE;

	return written;
}

Stream::openflag FileStreamImpl::CopyPath(char* dst, size_t len) const {
	Strncpy_s(dst, len, mFullPath.c_str(), mFullPath.size());
	
	return mFlag & loc_mask;
}

const char* FileStreamImpl::GetPath(openflag& loc) const {
	loc = mFlag & loc_mask;
	return mFullPath.c_str();
}

const char* FileStreamImpl::GetFilename() const  {
	return mFullPath.c_str();
}

} //namespace glf

